package ch.ethz.fcl.metrobuzz.render.accessbility.view.local;

import java.awt.Color;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.lwjgl.opengl.GL11;

import ch.ethz.fcl.metrobuzz.MBContainer;
import ch.ethz.fcl.metrobuzz.algorithm.network.shortest_path.public_transport.PathPublic;
import ch.ethz.fcl.metrobuzz.algorithm.network.shortest_path.public_transport.Walk;
import ch.ethz.fcl.metrobuzz.applications.AccessibilityApp;
import ch.ethz.fcl.metrobuzz.data.scene.basemap.Node;
import ch.ethz.fcl.metrobuzz.data.texture.MBTexture;
import ch.ethz.fcl.metrobuzz.data.transit.Stop;
import ch.ethz.fcl.metrobuzz.render.accessbility.AccessibilityRender;
import ch.ethz.fcl.metrobuzz.render.techniques.projection.Projection;
import ch.ethz.fcl.metrobuzz.spatialindex.object2d.Point2D;
import ch.ethz.fcl.metrobuzz.spatialindex.object2d.Polygon;
import ch.ethz.fcl.metrobuzz.spatialindex.object2d.Rectangle;
import ch.ethz.fcl.metrobuzz.spatialindex.object3d.Point3D;
import ch.ethz.fcl.metrobuzz.tools.MBGL;
import ch.ethz.fcl.metrofont.Pen;

/**
 * Visualizing the isochrones that shows the accessible regions from an origin
 * within a time period
 * 
 * 
 * @author Zeng Wei
 * @email zeng@arch.ethz.ch
 * 
 */

public class IsochroneView {
	private MBContainer container;

	private Isochrone halfHourIso;
	private Isochrone oneHourIso;

	public IsochroneView(MBContainer container, AccessibilityRender accRen) {
		this.container = container;

		halfHourIso = new Isochrone(container, accRen, 30 * 60, Color.BLUE);
		oneHourIso = new Isochrone(container, accRen, 60 * 60, Color.PINK);
	}

	public void update() {
		halfHourIso.update();
		oneHourIso.update();
	}

	public void renderOutLine() {
		oneHourIso.renderOutBound();
		halfHourIso.renderOutBound();

		renderIsoMark();
	}

	public void renderInBound() {
		oneHourIso.setColor(Color.MAGENTA);

		oneHourIso.renderInBound();
		halfHourIso.renderInBound();
	}

	private void renderIsoMark() {
		Projection proj = container.getViewer().getProjection();

		int width = 30;
		int screenWidth = AccessibilityApp.CANVAS_WIDTH;

		int height = 20;
		if (AccessibilityRender.FLOW_MAP_VIEW)
			height = 100;

		Point3D p = proj.getWorldCoord(new Point2D(screenWidth - 318, height),
				0);
		float length = proj.getWorldDistance(width);

		renderShadowBackground(container, p.getX() - length / 18, p.getY()
				- length / 7, p.getX() + length * 9.7f, p.getY() + length
				* 2.2f);

		renderIsoMark(p.getX() + length / 5, p.getY(), length,
				halfHourIso.getColor(), "30 min");
		renderIsoMark(p.getX() + length / 5 + length * 4, p.getY(), length,
				oneHourIso.getColor(), "60 min");

		float worldFontSize = container.getViewer().getProjection()
				.getWorldDistance(20);

		container.getFont().display(
				"Accessible Regions",
				new Pen(p.getX() + length / 5, p.getY() + length * 1.3f,
						worldFontSize));
	}

	protected void renderShadowBackground(MBContainer container, float minX,
			float minY, float maxX, float maxY) {
		Rectangle shadowRegion = new Rectangle(minX, minY, maxX, maxY);
		MBTexture.renderTexture(container.getData().getTexture().getShadow(),
				shadowRegion);
	}

	private void renderIsoMark(float x, float y, float length, Color color,
			String label) {
		float minX = x;
		float minY = y;
		float maxX = minX + length;
		float maxY = minY + length;

		float r = color.getRed() / 255.0f, g = color.getGreen() / 255.0f, b = color
				.getBlue() / 255.0f;
		GL11.glColor3f(r, g, b);

		GL11.glBegin(GL11.GL_POLYGON);
		GL11.glVertex2f(minX, minY);
		GL11.glVertex2f(minX, maxY);
		GL11.glVertex2f(maxX, maxY);
		GL11.glVertex2f(maxX, minY);
		GL11.glEnd();

		GL11.glColor3f(0, 0, 0);
		GL11.glLineWidth(1.0f);
		GL11.glBegin(GL11.GL_LINE_STRIP);
		GL11.glVertex2f(minX, minY);
		GL11.glVertex2f(minX, maxY);
		GL11.glVertex2f(maxX, maxY);
		GL11.glVertex2f(maxX, minY);
		GL11.glVertex2f(minX, minY);
		GL11.glEnd();

		float worldFontSize = container.getViewer().getProjection()
				.getWorldDistance(20);

		container.getFont().display(
				label,
				new Pen(maxX + worldFontSize / 4, minY + length / 4,
						worldFontSize));
	}

	public Isochrone getHalfHourIsochrone() {
		return halfHourIso;
	}

	public Isochrone getOneHourIsochrone() {
		return oneHourIso;
	}

	class Isochrone {
		private MBContainer container;
		private AccessibilityRender accRen;

		private float timeLimit = 60 * 60; // default time limit is 1 hour

		private Color color = Color.RED; // default color white

		private List<Stop> stops;

		private Map<Stop, Float> stopRadiusMap = null;
		private Map<Stop, Float> stopSpeedMap = null;

		private Map<Node, Boolean> nodeInMap = null;

		public Isochrone(MBContainer container, AccessibilityRender accRen) {
			this.container = container;
			this.accRen = accRen;
			stops = container.getData().getPublicTransit().getStops();
			nodeInMap = new HashMap<Node, Boolean>();
		}

		public Isochrone(MBContainer container, AccessibilityRender accRen,
				float timeLimit, Color color) {
			this(container, accRen);
			this.timeLimit = timeLimit;
			this.color = color;
		}

		public void setColor(Color color) {
			this.color = color;
		}

		public void update() {
			Map<Stop, PathPublic> pathMap = accRen.getFlowMapView()
					.getPathMap();

			if (pathMap == null)
				return;

			float walkSpeed = container.getData().getScene().getBound()
					.getScreenDist(Walk.SPEED);

			stopRadiusMap = new HashMap<Stop, Float>();
			stopSpeedMap = new HashMap<Stop, Float>();
			nodeInMap = new HashMap<Node, Boolean>();

			for (Stop s : stops) {
				PathPublic path = pathMap.get(s);
				if (path == null)
					continue;

				float cost = path.getCost();
				if (cost >= timeLimit)
					continue;

				float walkTime = timeLimit - cost;
				walkTime = Math.min(walkTime, Walk.MAXIMUM_WALK_LIMIT);

				float radius = walkTime * walkSpeed;
				stopRadiusMap.put(s, radius);

				float speed = path.getDist() / path.getCost();
				stopSpeedMap.put(s, speed);
			}

			for (Node n : container.getData().getScene().getNodes()) {
				for (Stop s : stopRadiusMap.keySet()) {
					float radius = stopRadiusMap.get(s);
					float dist = s.distance(n);
					if (dist <= radius) {
						nodeInMap.put(n, true);
						break;
					}
				}
			}
		}

		public void renderOutBound() {
			if (stopRadiusMap == null)
				return;

			setStenciOutBound();

			float pixelDist = container.getViewer().getProjection()
					.getWorldDistance(1);

			float r = color.getRed() / 255.0f;
			float g = color.getGreen() / 255.0f;
			float b = color.getBlue() / 255.0f;

			GL11.glColor4f(r, g, b, 1f);
			for (Stop s : stopRadiusMap.keySet()) {
				float radius = stopRadiusMap.get(s);

				float thickness = stopSpeedMap.get(s) * pixelDist * 3f;

				if (thickness > radius * 2)
					thickness = Math.min(thickness, radius * 1.5f);

				MBGL.renderEclipse(s.getX(), s.getY(), 0.1f,
						radius + thickness, radius + thickness, true);
			}

			GL11.glDisable(GL11.GL_STENCIL_TEST);
		}

		public void renderInBound() {
			if (stopRadiusMap == null)
				return;

			setStenciInBound();

			float r = color.getRed() / 255.0f;
			float g = color.getGreen() / 255.0f;
			float b = color.getBlue() / 255.0f;

			GL11.glColor4f(r, g, b, 0.5f);
			// set paintable for whole screen
			GL11.glBegin(GL11.GL_POLYGON);
			GL11.glVertex3f(-1, -1, 0);
			GL11.glVertex3f(-1, 1, 0);
			GL11.glVertex3f(1, 1, 0);
			GL11.glVertex3f(1, -1, 0);
			GL11.glEnd();

			GL11.glDisable(GL11.GL_STENCIL_TEST);
		}

		private void setStenciOutBound() {
			GL11.glClear(GL11.GL_STENCIL_BUFFER_BIT);
			GL11.glEnable(GL11.GL_STENCIL_TEST);

			GL11.glColorMask(false, false, false, false);

			GL11.glStencilFunc(GL11.GL_ALWAYS, 1, 1);
			GL11.glStencilOp(GL11.GL_REPLACE, GL11.GL_REPLACE, GL11.GL_REPLACE);

			// set paintable for whole screen
			GL11.glBegin(GL11.GL_POLYGON);
			GL11.glVertex3f(-1, -1, 0);
			GL11.glVertex3f(-1, 1, 0);
			GL11.glVertex3f(1, 1, 0);
			GL11.glVertex3f(1, -1, 0);
			GL11.glEnd();

			GL11.glStencilFunc(GL11.GL_ALWAYS, 0, 0);
			GL11.glStencilOp(GL11.GL_REPLACE, GL11.GL_REPLACE, GL11.GL_REPLACE);

			// set not paintable for within in areas
			for (Stop s : stopRadiusMap.keySet()) {
				float radius = stopRadiusMap.get(s);

				GL11.glColor4f(0, 0, 0, 1f);

				MBGL.renderEclipse(s.getX(), s.getY(), 0, radius, radius, true);
			}

			// render setting
			GL11.glColorMask(true, true, true, true);
			GL11.glStencilFunc(GL11.GL_EQUAL, 1, 1);
			GL11.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_KEEP);
		}

		private void setStenciInBound() {
			GL11.glClear(GL11.GL_STENCIL_BUFFER_BIT);
			GL11.glEnable(GL11.GL_STENCIL_TEST);

			GL11.glColorMask(false, false, false, false);

			GL11.glStencilFunc(GL11.GL_ALWAYS, 0, 0);
			GL11.glStencilOp(GL11.GL_REPLACE, GL11.GL_REPLACE, GL11.GL_REPLACE);

			// set not paintable for whole screen
			GL11.glBegin(GL11.GL_POLYGON);
			GL11.glVertex3f(-1, -1, 0);
			GL11.glVertex3f(-1, 1, 0);
			GL11.glVertex3f(1, 1, 0);
			GL11.glVertex3f(1, -1, 0);
			GL11.glEnd();

			GL11.glStencilFunc(GL11.GL_ALWAYS, 1, 1);
			GL11.glStencilOp(GL11.GL_REPLACE, GL11.GL_REPLACE, GL11.GL_REPLACE);

			// set paintable for within in areas
			GL11.glColor4f(0, 0, 0, 1f);
			for (Stop s : stopRadiusMap.keySet()) {
				float radius = stopRadiusMap.get(s);

				MBGL.renderEclipse(s.getX(), s.getY(), 0, radius, radius, true);
			}

			if (timeLimit == 60 * 60) {
				GL11.glStencilFunc(GL11.GL_ALWAYS, 0, 0);
				GL11.glStencilOp(GL11.GL_REPLACE, GL11.GL_REPLACE,
						GL11.GL_REPLACE);

				Isochrone halfHourIso = accRen.getIsochroneView()
						.getHalfHourIsochrone();
				Map<Stop, Float> halfHourStopRadius = halfHourIso.stopRadiusMap;
				// set not paintable for within in areas
				GL11.glColor4f(0, 0, 0, 1f);
				for (Stop s : halfHourStopRadius.keySet()) {
					float radius = halfHourStopRadius.get(s);

					MBGL.renderEclipse(s.getX(), s.getY(), 0, radius, radius,
							true);
				}
			}

			// render setting
			GL11.glColorMask(true, true, true, true);
			GL11.glStencilFunc(GL11.GL_EQUAL, 1, 1);
			GL11.glStencilOp(GL11.GL_KEEP, GL11.GL_KEEP, GL11.GL_KEEP);
		}

		public float getTimeLimit() {
			return timeLimit;
		}

		public Color getColor() {
			return color;
		}

		public Map<Node, Boolean> getNodeInMap() {
			return nodeInMap;
		}
	}
}
